This example is for the FOREIGN KEY constraints. The tables in SQL can be created by $(D $(D sql CREATE TABLE Authors ( AuthorId INTEGER NOT NULL PRIMARY KEY, Name TEXT ); CREATE TABLE Books ( BookId INTEGER NOT NULL PRIMARY KEY, Title TEXT, AuthorId INTEGER, FOREIGN KEY (AuthorId) REFERENCES Authors(AuthorId) ); ))
The Books table now must have values in Books.AuthorId that are in Authors.AuthorId. By default if Authors deletes or updates its AuthorId and Books references the AuthorId, an exception is thrown. If you would like Books to cascade the effects instead you would create the Books table like $(D $(D sql CREATE TABLE Books ( BookId INTEGER NOT NULL PRIMARY KEY, Title TEXT, AuthorId INTEGER, FOREIGN KEY (AuthorId) REFERENCES Authors(AuthorId) ON DELETE CASCADE ON UPDATE CASCADE ); ))
This package can do any of the update or delete rules. For more rules information look at $(WIKI constraints, Rule).
Below I will create the classes. I will use the cascaded Books for this example.
1 import db_constraints; 2 3 class Author 4 { 5 private int _AuthorId; 6 @PrimaryKeyColumn @NotNull 7 @property int AuthorId() 8 { 9 return _AuthorId; 10 } 11 @property void AuthorId(int value) 12 { 13 setter(_AuthorId, value); 14 } 15 private string _Name; 16 @property string Name() 17 { 18 return _Name; 19 } 20 @property void Name(string value) 21 { 22 setter(_Name, value); 23 } 24 this(int AuthorId_, string Name_) 25 { 26 this._AuthorId = AuthorId_; 27 this._Name = Name_; 28 initializeKeyedItem(); 29 } 30 31 // we must define dup() since we are going 32 // to change AuthorId which is our implied 33 // clustered index 34 Author dup() 35 { 36 return new Author(this._AuthorId, this._Name); 37 } 38 39 mixin KeyedItem!(); 40 } 41 class Authors 42 { 43 mixin KeyedCollection!(Author); 44 } 45 46 // adding in this function so we can get multiple 47 // author records at once 48 Authors GetAuthorsFromDB() 49 { 50 return new Authors([ 51 new Author(1, "Jane Austen"), 52 new Author(2, "Leo Tolstoy"), 53 new Author(3, "Joseph Heller"), 54 new Author(4, "Charles Dickens") 55 ]); 56 } 57 58 59 // we could put Books in a different file 60 // to do that you would just need to import the file where Authors is. 61 62 // attach the foreign key constraint attribute to the singular class 63 @ForeignKeyConstraint!( 64 ["AuthorId"], /* Book column */ 65 "Authors", /* referenced table which is Authors in this case */ 66 ["AuthorId"], /* referenced column which is Authors.AuthorId */ 67 Rule.cascade, /* what to do when we update Authors.AuthorId */ 68 Rule.cascade) /* what to do when we delete Authors.AuthorId */ 69 class Book 70 { 71 private int _BookId; 72 @PrimaryKeyColumn @NotNull 73 @property int BookId() 74 { 75 return _BookId; 76 } 77 @property void BookId(int value) 78 { 79 setter(_BookId, value); 80 } 81 private string _Title; 82 @property string Title() 83 { 84 return _Title; 85 } 86 @property void Title(string value) 87 { 88 setter(_Title, value); 89 } 90 private int _AuthorId; 91 @property int AuthorId() 92 { 93 return _AuthorId; 94 } 95 @property void AuthorId(int value) 96 { 97 setter(_AuthorId, value); 98 } 99 this(int BookId_, string Title_, int AuthorId_) 100 { 101 this._BookId = BookId_; 102 this._Title = Title_; 103 this._AuthorId = AuthorId_; 104 initializeKeyedItem(); 105 } 106 107 mixin KeyedItem!(); 108 } 109 110 class Books 111 { 112 mixin KeyedCollection!(Book); 113 } 114 Books GetBooksFromDB() 115 { 116 return new Books([ 117 new Book(1, "Emma", 1), 118 new Book(2, "War and Peace", 2), 119 new Book(3, "Catch XII", 3), 120 new Book(4, "David Copperfield", 4), 121 new Book(5, "Good as Gold", 3), 122 new Book(6, "Anna Karenia", 2) 123 ]); 124 } 125 126 127 // we will get both collections 128 // and then associate authors to books 129 130 // ON UPDATE CASCADE 131 { 132 auto authors = GetAuthorsFromDB(); 133 auto books = GetBooksFromDB(); 134 135 // when we associate authors to books 136 // there should be no exceptions since 137 // we are starting with correct data 138 import std.exception : assertNotThrown, assertThrown; 139 assertNotThrown!ForeignKeyException(books.authors = authors); 140 // books.authors is a write-only property made by 141 // mixin KeyedCollection!(Book); 142 // you can also set books.authors = null when you want to 143 // remove the association 144 145 // if you recall from before we can use the primary key to 146 // search our collections easily. In Books, Emma has BookId 1. 147 assert(books[1].Title == "Emma"); 148 assert(books[1].BookId == 1); 149 150 // Emma is written by Jane Austen and 151 // in authors Jane Austen has AuthorId 1 152 assert(authors[1].Name == "Jane Austen"); 153 assert(authors.contains(books[1].AuthorId)); 154 assert(authors[1].AuthorId == books[1].AuthorId); 155 156 // lets say somehow we changed Jane Austen to have AuthorId 5 157 // since we have on update cascade for books we would expect 158 // Emma to get AuthorId 5 159 authors[1].AuthorId = 5; 160 // this will also change authors to no longer have key 1 161 assert(authors.contains(5) && !authors.contains(1)); 162 // Emma still has BookId 1 since that did not change but 163 // should have AuthorId 5 164 assert(books[1].Title == "Emma"); 165 assert(books[1].AuthorId == 5); 166 167 // We were able to change Jane Austen's AuthorId since we 168 // defined dup in Author. We did not define dup in Books 169 // which means if we change BookId we should expect a 170 // KeyedException 171 assertThrown!KeyedException(books[1].BookId = 7); 172 173 // it is good but not necessary to set books.authors to null when you 174 // leave scope just to disconnect signals and associations 175 books.authors = null; 176 } 177 178 // ON DELETE CASCADE 179 { 180 auto authors = GetAuthorsFromDB(); 181 auto books = GetBooksFromDB(); 182 books.authors = authors; 183 184 // we have 4 authors 185 assert(authors.length == 4); 186 // and 6 books 187 assert(books.length == 6); 188 189 import std.algorithm : count; 190 // there are two books that have author id 3 191 auto booksWithAuthorId3 = 192 books.byValue.count!((a, b) => a.AuthorId == b)(3); 193 194 assert(booksWithAuthorId3 == 2); 195 196 // this means if we delete AuthorId 3 from authors and 197 // we have on delete cascade for books we should expect 198 // books to have length 4 and authors to have length 3 199 authors.remove(3); 200 assert(authors.length == 3); 201 assert(books.length == 4); 202 203 booksWithAuthorId3 = books.byValue.count!((a, b) => a.AuthorId == b)(3); 204 assert(booksWithAuthorId3 == 0); 205 206 books.authors = null; 207 } 208 209 // ON DELETE RESTRICT 210 { 211 import std.exception : assertThrown, assertNotThrown; 212 auto authors = GetAuthorsFromDB(); 213 auto books = GetBooksFromDB(); 214 books.authors = authors; 215 216 // you can change the on update and on delete rule for your foreign key 217 // by using the foreign key name and _UpdateRule or _DeleteRule...we 218 // did not name ours so it got the default fk_Book_Authors 219 220 // lets say we want to restrict authors deletion now 221 books.fk_Book_Authors_DeleteRule = Rule.restrict; 222 223 // since we have 2 records in books that reference AuthorId 3 we should 224 // get an exception when deleting AuthorId 3 from authors 225 assertThrown!ForeignKeyException(authors.remove(3)); 226 227 // now we can unreference our table and remove 3 without errors 228 books.authors = null; 229 assertNotThrown!ForeignKeyException(authors.remove(3)); 230 231 // but now when we try to re-associate we will get an exception 232 assertThrown!ForeignKeyException(books.authors = authors); 233 }